/**
* Copyright (c) 2012-2013 Nokia Corporation. All rights reserved.
* Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation. 
* Oracle and Java are trademarks or registered trademarks of Oracle and/or its
* affiliates. Other product and company names mentioned herein may be trademarks
* or trade names of their respective owners. 
* See LICENSE.TXT for license information.
*/

package com.nokia.example.explonoid.game;

import com.nokia.example.explonoid.ExplonoidCanvas;
import com.nokia.example.explonoid.Main;
import com.nokia.example.explonoid.effects.LightManager;
import com.nokia.example.explonoid.effects.Slideable;
import com.nokia.example.explonoid.effects.Shaker;
import com.nokia.example.explonoid.effects.ShockWaveManager;
import com.nokia.example.explonoid.effects.SparkManager;
import com.nokia.example.explonoid.sensors.AccelerationProvider;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Random;
import javax.microedition.lcdui.game.LayerManager;
import javax.microedition.lcdui.game.Sprite;
import java.util.Vector;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Font;
import javax.microedition.lcdui.Graphics;

/**
 * Handles the core functionality of gameplay.
 * <P>
 * Core functionality includes e.g. collision detection, 
 * pointer actions, and plate movement.
 */
public class Game
        extends LayerManager
        implements Slideable {

    public static final int STATE_MENU = 0;
    public static final int STATE_INFO = 1;
    public static final int STATE_TRANSITION = 2;
    public static final int STATE_LEVEL = 3;
    public static final int STATE_GAME_OVER = 4;
    public static final int STATE_HIGH_SCORES = 5;
    public static final int EVENT_BRICK_COLLISION = 0;
    public static final int EVENT_BRICK_EXPLOSION = 1;
    public static final int EVENT_PLATE_COLLISION = 2;
    public static final int EVENT_WALL_COLLISION = 3;
    public static final int EVENT_LEVEL_CHANGE = 4;
    public static final int EVENT_BUTTON_PRESSED = 5;
    public static final int EVENT_BALL_OUT = 6;
    public static final int EVENT_BONUS = 7;
    public static final int EVENT_LEVEL_STARTED = 8;
    public static final int EVENT_GAME_OVER = 9;
    private static final int LIVES = 5;
    private static final int WALL_PADDING = 4;
    private final int IN_X;
    private final int OUT_X;
    private int levelNumber;
    private int plateY;
    private int cornerX;
    private int cornerY;
    private int gameWidth;
    private int gameHeight;
    private int pointerX;
    private double aX = 0;
    private double vX = 0;
    private boolean pointerPressed;
    private boolean menuPressed = false;
    private boolean changeLevel;
    private boolean aiming = true;
    private boolean sensorsEnabled = true;
    private AccelerationProvider accelerationProvider;
    private Resources r;
    private Plate plate;
    private Ball ball;
    private BrickGrid bricks;
    private Sprite leftWall;
    private Sprite rightWall;
    private Sprite topWall;
    private Sprite dashboard;
    private Sprite menuButton;
    private Sprite newGameButton;
    private Shaker shaker;
    private ShockWaveManager shockWaveManager;
    private SparkManager sparkManager;
    private Vector lives;
    private Random rnd = new Random();
    private Font font = Font.getFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_SMALL);
    private final Listener listener;

    public interface Listener {

        void changeState(int state);

        void handleEvent(int event);
    }

    /**
     * Constructor
     * @param cornerX Horizontal position of the top left coordinate of the game area
     * @param cornerY Vertical position of the top left coordinate of the game area
     * @param width Width of the Game area
     * @param height Height of the Game area
     * @param r Game resources
     * @param listener Game event listener
     */
    public Game(int cornerX, int cornerY, int width, int height, Resources r, Listener listener) {
        this.listener = listener;
        this.gameWidth = width;
        this.gameHeight = height;
        this.r = r;

        IN_X = cornerX;
        OUT_X = cornerX + width;

        this.cornerX = OUT_X;
        this.cornerY = cornerY;

        createGame();
        setViewWindow(0, 0, width, height);
    }

    /**
     * Creates and initializes the game assets
     */
    private void createGame() {
        lives = new Vector();

        // Create Bricks
        bricks = new BrickGrid(this, gameWidth, gameHeight, r);

        // Create Plate
        plate = new Plate(gameWidth, r);
        plateY = gameHeight - plate.getHeight() - r.dashboard.getHeight() / 2;
        plate.setPosition((gameWidth - plate.getWidth()) / 2, plateY);

        // Create Ball
        ball = new Ball(r);

        // Create Walls
        topWall = new Sprite(r.horizontalWall, r.horizontalWall.getWidth(), r.horizontalWall.getHeight() / 2);
        leftWall = new Sprite(r.verticalWall, r.verticalWall.getWidth() / 2, r.verticalWall.getHeight());
        rightWall = new Sprite(r.verticalWall, r.verticalWall.getWidth() / 2, r.verticalWall.getHeight());

        int wallPadding = r.scale(WALL_PADDING);
        topWall.defineCollisionRectangle(0, 0, gameWidth,
                                         topWall.getHeight() - wallPadding);
        leftWall.defineCollisionRectangle(0, 0,
                                          leftWall.getWidth() - wallPadding, leftWall.getHeight() - wallPadding);
        rightWall.defineCollisionRectangle(wallPadding, 0,
                                           rightWall.getWidth() - wallPadding, rightWall.getHeight() - wallPadding);

        topWall.setPosition(0, 0);
        leftWall.setPosition(r.scale(-3), 0);
        rightWall.setPosition(gameWidth + r.scale(3) - rightWall.getWidth(), 0);

        // Create Dashboard
        dashboard = new Sprite(r.dashboard);
        dashboard.setPosition(0, gameHeight - dashboard.getHeight());

        // Create Buttons
        menuButton = new Sprite(r.menuButton);
        menuButton.setPosition(r.scale(180), gameHeight - menuButton.getHeight());
        newGameButton = new Sprite(r.newGameButton);
        newGameButton.setPosition(r.scale(10), gameHeight - newGameButton.getHeight());

        // Append sprites to LayerManager
        append(plate);
        append(topWall);
        append(leftWall);
        append(rightWall);
        append(ball);
        pointerPressed = false;

        // Create effect managers        
        shockWaveManager = new ShockWaveManager();
        sparkManager = new SparkManager(24, r, this);
        shaker = new Shaker();
    }

    /**
     * Render game graphics
     */
    public void paint(Graphics g) {
        shockWaveManager.paint(g);
        paint(g, cornerX + shaker.magnitudeX, cornerY + shaker.magnitudeY);
        if (lives.isEmpty()) {
            g.drawImage(r.gameOver, cornerX + gameWidth / 2,
                        cornerY + gameHeight / 2, Graphics.VCENTER | Graphics.HCENTER);
        }
        g.setColor(0xFFFFFFFF);
        g.setFont(font);
        g.drawString("" + levelNumber, cornerX + shaker.magnitudeX + gameWidth / 2,
                     cornerY + shaker.magnitudeY + gameHeight, Graphics.BOTTOM | Graphics.HCENTER);
        topWall.setFrame(0);
        leftWall.setFrame(0);
        rightWall.setFrame(0);
    }

    /**
     * Update game logic
     */
    public void update() {
        if (!lives.isEmpty()) {
            if (aiming) {
                aim();
            }
            else {
                ball.move();

                // Ball out of screen
                if (ball.getY() > gameHeight) {
                    sparkManager.burst(ball.getRefPixelX(), gameHeight);
                    int refX = cornerX + ball.getRefPixelX();
                    int refY = cornerY + ball.getRefPixelY();
                    shockWaveManager.createShockWave(refX, refY, r.scale(900), 3);
                    shockWaveManager.createShockWave(refX, refY, r.scale(500), 4);
                    shockWaveManager.createShockWave(refX, refY, r.scale(100), 6);
                    vibrate(1000);
                    LightManager.pulse(3);
                    Sprite life = (Sprite) lives.lastElement();
                    lives.removeElement(life);
                    remove(life);
                    if (lives.isEmpty()) {
                        newGameButton.setVisible(true);
                        listener.handleEvent(EVENT_GAME_OVER);
                    }
                    else {
                        placeBall();
                    }
                    listener.handleEvent(EVENT_BALL_OUT);
                }
            }
            movePlate();
            checkCollisions();
        }
    }

    /**
     * Keep ball attached to the plate
     */
    private void aim() {
        ball.setPosition(plate.getRefPixelX() - ball.getWidth() / 2, plate.getTopY() - ball.getHeight());
    }

    /**
     * Set ball to its starting position and adjust velocity to
     */
    public void placeBall() {
        ball.setVelocityX(0);
        ball.setVelocityY(r.scale(-4 - levelNumber / 3) - 1);
        aiming = true;
        aim();
    }

    /**
     * Check collisions
     */
    public void checkCollisions() {
        checkPlateCollision();
        checkWallCollisions();
        checkBrickCollisions();
    }

    /**
     * Check and act for collisions between the ball and the plate
     */
    public void checkPlateCollision() {
        if (ball.collidesWith(plate, false)) {
            if (ball.getRefPixelY() < plate.getRefPixelY()) {
                ball.bounceUp();
                // Calculate horizontal velocity relative to the distance
                // between plate center and collision point
                ball.setVelocityX((int) Math.ceil(r.scale(-8.0)
                        * (plate.getRefPixelX() - ball.getRefPixelX())
                        / (plate.getPlateWidth())));
                listener.handleEvent(EVENT_PLATE_COLLISION); // Inform listener
            }
        }
    }

    /**
     * Check and act for collisions between the ball and the walls
     */
    public void checkWallCollisions() {
        boolean collision = false;
        if (ball.collidesWith(topWall, false) || ball.getY() < 0) {
            ball.bounceDown();
            topWall.setFrame(1); // Glow pulse
            collision = true;
        }
        if (ball.collidesWith(leftWall, false) || ball.getX() < 0) {
            ball.bounceRight();
            leftWall.setFrame(1); // Glow pulse
            collision = true;
        }
        else if (ball.collidesWith(rightWall, false)
                || ball.getX() + ball.getWidth() > gameWidth) {
            ball.bounceLeft();
            rightWall.setFrame(1); // Glow pulse
            collision = true;
        }
        if (collision) {
            listener.handleEvent(EVENT_WALL_COLLISION); // Inform listener
        }
    }

    /**
     * Check and act for collisions between the ball and the bricks
     */
    public void checkBrickCollisions() {
        // Check and get the brick that the ball collided with
        Brick brick = bricks.collidesWith(ball);
        if (brick != null) { // Check the bounce accordign to the hit position
            if (ball.getRefPixelY() < brick.getRefPixelY()) {
                ball.bounceUp();
            }
            else {
                ball.bounceDown();
            }
            if (ball.getRefPixelX() < brick.getX()) {
                ball.bounceLeft();
            }
            else if (ball.getRefPixelX() > brick.getX() + brick.getWidth()) {
                ball.bounceRight();
            }

            // If the brick explodes...
            if (brick.getFrame() % 2 == 0) {
                remove(brick);
                ball.changeVelocityX(rnd.nextInt(2));
                sparkManager.burst(brick.getRefPixelX(), brick.getRefPixelY());
                listener.handleEvent(EVENT_BRICK_EXPLOSION);
                vibrate(40);
                LightManager.pulse(2);
                if (bricks.isEmpty()) {
                    changeLevel = true;
                    listener.changeState(STATE_LEVEL);
                    listener.handleEvent(EVENT_LEVEL_CHANGE);
                }
                else {
                    shaker.shake(ball.getVelocityX(), ball.getVelocityY());
                }
            }
            else {
                listener.handleEvent(EVENT_BRICK_COLLISION);
            }
        }
    }

    /**
     * Handle pointer pressed
     */
    public void pointerPressed(int x, int y) {
        setPlateDestination(x);
        if (menuButtonHit(x, y)) {
            menuPressed = true;
        }
    }

    /**
     * Handle pointer released
     */
    public void pointerReleased(int x, int y) {
        stopMovement();
        if (menuPressed && menuButtonHit(x, y)) {
            rightButtonPressed();
        }
        else {
            menuPressed = false;
            if (aiming) {
                aiming = false;
            }
        }
        if (newGameButton.isVisible() && newGameButtonHit(x, y)) {
            leftButtonPressed();
        }
    }

    /**
     * Handle pointer dragged
     */
    public void pointerDragged(int x, int y) {
        setPlateDestination(x);
    }

    /**
     * Handle firing
     */
    public void fire() {
        if (aiming) {
            aiming = false; // Release the ball
        }
    }

    /**
     * Handle left action key
     */
    public void leftButtonPressed() {
        if (lives.isEmpty()) {
            listener.changeState(STATE_LEVEL);
            newGame();
        }
    }

    /**
     * Handle right action key
     */
    public void rightButtonPressed() {
        listener.changeState(STATE_MENU);
        listener.handleEvent(EVENT_BUTTON_PRESSED);
    }

    /**
     * Set the plate moving towards given x coordinate
     */
    public void setPlateDestination(int x) {
        pointerPressed = true;
        plate.setMoving(x);
        pointerX = x;
    }

    /**
     * Moves plate towards set destination or according to acceleration sensors
     */
    public void movePlate() {
        if (pointerPressed) {
            int distance = plate.getRefPixelX() - pointerX;
            if (distance > 0 && plate.getLeftX() > 0
                    || distance < 0 && plate.getRightX() < gameWidth) {
                distance *= 0.94; // Shorten the distance with easing
            }
            plate.setRefPixelPosition(pointerX + distance, plate.getRefPixelY());
            if (distance == 0) {
                stopMovement();
            }
        }
        else if (accelerationProvider != null) {
            // Calculate velocity according to acceleration on x-axis and convert
            vX -= gameWidth * aX * ExplonoidCanvas.INTERVAL / 5000;
            if (Math.abs(vX) > 1) {
                if (vX < 0 && plate.getLeftX() > 0 || vX > 0 && plate.getRightX() < gameWidth) {
                    if (plate.getLeftX() + vX < 0) {
                        vX = -plate.getLeftX();
                    }
                    else if (plate.getRightX() + vX > gameWidth) {
                        vX = gameWidth - plate.getRightX();
                    }
                    plate.move((int) vX, 0);
                }
                vX = 0.0;
            }
        }
    }

    /**
     * Moves plate one constant step to the left
     */
    public void movePlateLeft() {
        if (plate.getLeftX() > 0) {
            plate.moveLeft();
        }
    }

    /**
     * Moves plate one constant step to the right
     */
    public void movePlateRight() {
        if (plate.getRightX() < gameWidth) {
            plate.moveRight();
        }
    }

    public void stopMovement() {
        pointerPressed = false;
        plate.stop();
    }

    /**
     * Vibrate device
     * @param duration
     */
    public void vibrate(int duration) {
        Display display = Display.getDisplay(Main.getInstance());
        display.vibrate(duration);
    }

    private boolean menuButtonHit(int x, int y) {
        return containsPoint(x, y, menuButton);
    }

    private boolean newGameButtonHit(int x, int y) {
        return containsPoint(x, y, newGameButton);
    }

    /**
     * Checks whether a coordinate overlaps the bounding rectangle of a sprite
     */
    private boolean containsPoint(int x, int y, Sprite sprite) {
        return x >= sprite.getX() && x <= sprite.getX() + sprite.getWidth()
                && y >= sprite.getY();
    }

    /**
     * Starts a new game
     */
    public void newGame() {
        levelNumber = 0;
        initDashboard(LIVES);
        nextLevel();
    }

    /**
     * Loads the next level
     */
    public void nextLevel() {
        placeBall();
        levelNumber++;
        loadLevel(levelNumber);
    }

    /**
     * Loads up a level
     * @param number
     */
    public void loadLevel(int number) {
        bricks.load(this, Levels.getLevel((number - 1) % Levels.LEVEL_COUNT + 1));
    }

    /**
     * Initializes the dashboard
     * @param balls Number of balls left
     */
    public void initDashboard(int balls) {
        if (balls > 0) {
            newGameButton.setVisible(false);
        }
        insert(dashboard, 0);
        insert(menuButton, 0);
        insert(newGameButton, 0);
        while (lives.size() > 0) {
            Sprite life = (Sprite) lives.lastElement();
            lives.removeElement(life);
            remove(life);
        }
        for (int i = 0; i < balls; i++) {
            Sprite life = new Sprite(r.life);
            life.setPosition(r.scale(12) + i * life.getWidth(), gameHeight - r.scale(14));
            lives.addElement(life);
            insert(life, 0);
        }
        if (lives.size() > 0) {
            ((Sprite) (lives.elementAt(0))).setVisible(false);
        }
    }

    /**
     * Serializes the game data
     * @return Game data as a byte array
     */
    public byte[] getSnapshot() {
        ByteArrayOutputStream bout = null;
        try {
            bout = new ByteArrayOutputStream();
            DataOutputStream dout = new DataOutputStream(bout);
            dout.writeInt(levelNumber);
            dout.writeInt(lives.size());
            dout.writeBoolean(aiming);
            dout.writeInt(plate.getX());
            dout.writeInt(plate.getY());
            dout.writeInt(ball.getX());
            dout.writeInt(ball.getY());
            dout.writeInt(ball.getVelocityX());
            dout.writeInt(ball.getVelocityY());
            bricks.writeTo(dout);
            return bout.toByteArray();
        }
        catch (IOException e) {
        }
        finally {
            try {
                if (bout != null) {
                    bout.close();
                }
            }
            catch (IOException e) {
            }
        }
        return new byte[0];
    }

    /**
     * Deserializes game data
     * @param record Game data in byte array
     * @return True, if loading was successful - otherwise false
     */
    public boolean load(byte[] record) {
        if (record == null) {
            return false;
        }
        try {
            DataInputStream din = new DataInputStream(new ByteArrayInputStream(record));
            levelNumber = din.readInt();
            initDashboard(din.readInt());
            aiming = din.readBoolean();
            plate.setPosition(din.readInt(), din.readInt());
            ball.setPosition(din.readInt(), din.readInt());
            ball.setVelocityX(din.readInt());
            ball.setVelocityY(din.readInt());
            bricks.load(this, bricks.readFrom(din));
            return true;
        }
        catch (IOException e) {
        }
        return false;
    }

    /**
     * Moves the view inwards
     */
    public boolean slideIn() {
        int distance = cornerX - IN_X;
        distance *= 0.8;
        cornerX = IN_X + distance;
        boolean sliding = distance != 0;
        if (!sliding && sensorsEnabled && !AccelerationProvider.sensorPromptDeclined) {
            accelerationProvider = AccelerationProvider.getProvider(
                    new AccelerationProvider.Listener() {

                        public void dataReceived(double ax, double ay, double az) {
                            aX = ax;
                        }
                    });
            listener.handleEvent(EVENT_LEVEL_STARTED);
        }
        return sliding;
    }

    /**
     * Moves the view outwards
     */
    public boolean slideOut() {
        if (accelerationProvider != null) {
            accelerationProvider.close();
            accelerationProvider = null;
        }

        int distance = cornerX - OUT_X;
        distance *= 0.8;
        cornerX = OUT_X + distance;
        if (distance == 0 && changeLevel) {
            changeLevel = false;
            nextLevel();
        }
        return distance != 0;
    }

    public void enableSensors(boolean value) {
        sensorsEnabled = value;
    }
}
